<!DOCTYPE html>
<html class="has-navbar-fixed-top">
<head>
    <meta charset="utf-8">
<title>开发Web应用 - wanzixin</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.css">


<link href="/Study/Java/Spring/zh-cn/%E5%BC%80%E5%8F%91Web%E5%BA%94%E7%94%A8/" rel="alternate" hreflang="en" />
    


<meta name="description" content="">





    <meta name="description" content="在Web开发一章，我们学习了JavaEE中Web开发的基础：Servlet，总结来说：  Servlet规范定义了几种标准组件：Servlet、JSP、Filter和Listener Servlet的标准组件总是运行在Servlet容器中，如Tomcat、Jetty、WebLogic等  直接使用Servlet进行Web开发好比直接在JDBC上操作数据库，比较繁琐。更好的方法是在Servlet基础">
<meta property="og:type" content="article">
<meta property="og:title" content="开发Web应用">
<meta property="og:url" content="https://wanzixin.github.io/Study/Java/Spring/zh-cn/%E5%BC%80%E5%8F%91Web%E5%BA%94%E7%94%A8/index.html">
<meta property="og:site_name" content="wanzixin">
<meta property="og:description" content="在Web开发一章，我们学习了JavaEE中Web开发的基础：Servlet，总结来说：  Servlet规范定义了几种标准组件：Servlet、JSP、Filter和Listener Servlet的标准组件总是运行在Servlet容器中，如Tomcat、Jetty、WebLogic等  直接使用Servlet进行Web开发好比直接在JDBC上操作数据库，比较繁琐。更好的方法是在Servlet基础">
<meta property="og:locale" content="zh_CN">
<meta property="article:published_time" content="2021-07-03T02:02:37.000Z">
<meta property="article:modified_time" content="2021-07-10T07:41:12.194Z">
<meta property="article:author" content="wanzixin">
<meta name="twitter:card" content="summary">





<link rel="icon" href="/favicon.png">


<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Ovo|Source+Code+Pro">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/css/lightgallery.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/css/justifiedGallery.min.css">


<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/atom-one-light.min.css">


<link rel="stylesheet" href="/css/style.css">


<script defer src="//use.fontawesome.com/releases/v5.0.8/js/all.js"></script>


    
    
    
    
    
    
    
    
    
    

    


<meta name="generator" content="Hexo 5.4.0"></head>
<body>
    
<nav class="navbar is-transparent is-fixed-top navbar-main" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a class="navbar-item navbar-logo" href="/zh-cn">
                
                    
                    wanzixin
                    
                
            </a>
            <div class="navbar-burger">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>
        
        <div class="navbar-menu navbar-start">
            
            <a class="navbar-item "
               href="/archives">Archives</a>
            
            <a class="navbar-item "
               href="/categories">Categories</a>
            
            <a class="navbar-item "
               href="/categories/Diary">Diary</a>
            
            <a class="navbar-item "
               href="/categories/Gallery">Gallery</a>
            
            <a class="navbar-item "
               href="/categories/Study">Study</a>
            
            <a class="navbar-item "
               href="/categories/Item">Item</a>
            
            <a class="navbar-item "
               href="/about">About</a>
            
        </div>
        
        <div class="navbar-menu navbar-end">
            
            <a class="navbar-item search" title="搜索" href="javascript:;">
                <i class="fas fa-search"></i>
            </a>
            
            
            <div class="navbar-item is-hoverable has-dropdown is-hidden-mobile is-hidden-tablet-only toc">
                <a class="navbar-item" title="目录">
                    <i class="fa fa-list"></i>
                </a>
                <div class="navbar-dropdown is-right">
                    
                    
                    
                    
                    <a class="navbar-item" href="#使用Spring-MVC">1&nbsp;&nbsp;<b>使用Spring MVC</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#配置Spring-MVC">1.1&nbsp;&nbsp;配置Spring MVC</a>
                    
                    
                    
                    <a class="navbar-item" href="#编写Controller">1.2&nbsp;&nbsp;编写Controller</a>
                    
                    
                    
                    <a class="navbar-item" href="#小结">1.3&nbsp;&nbsp;小结</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用REST">2&nbsp;&nbsp;<b>使用REST</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#集成Filter">3&nbsp;&nbsp;<b>集成Filter</b></a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用Interceptor">4&nbsp;&nbsp;<b>使用Interceptor</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#处理异常">4.1&nbsp;&nbsp;处理异常</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#处理CORS">5&nbsp;&nbsp;<b>处理CORS</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#使用-CrossOrigin">5.1&nbsp;&nbsp;使用@CrossOrigin</a>
                    
                    
                    
                    <a class="navbar-item" href="#使用CorsRegistry">5.2&nbsp;&nbsp;使用CorsRegistry</a>
                    
                    
                    
                    <a class="navbar-item" href="#使用CorsFilter">5.3&nbsp;&nbsp;使用CorsFilter</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#国际化">6&nbsp;&nbsp;<b>国际化</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#获取Locale">6.1&nbsp;&nbsp;获取Locale</a>
                    
                    
                    
                    <a class="navbar-item" href="#提取资源文件">6.2&nbsp;&nbsp;提取资源文件</a>
                    
                    
                    
                    <a class="navbar-item" href="#创建MessageSource">6.3&nbsp;&nbsp;创建MessageSource</a>
                    
                    
                    
                    <a class="navbar-item" href="#实现多语言">6.4&nbsp;&nbsp;实现多语言</a>
                    
                    
                    
                    <a class="navbar-item" href="#切换Locale">6.5&nbsp;&nbsp;切换Locale</a>
                    
                    
                    
                    <a class="navbar-item" href="#小结-1">6.5.1&nbsp;&nbsp;小结</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#异步处理">7&nbsp;&nbsp;<b>异步处理</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#使用Filter">7.1&nbsp;&nbsp;使用Filter</a>
                    
                    
                    <hr class="navbar-divider">
                    
                    
                    <a class="navbar-item" href="#使用WebSocket">8&nbsp;&nbsp;<b>使用WebSocket</b></a>
                    
                    
                    
                    <a class="navbar-item" href="#处理WebSocket连接">8.1&nbsp;&nbsp;处理WebSocket连接</a>
                    
                    
                    
                    <a class="navbar-item" href="#客户端开发">8.2&nbsp;&nbsp;客户端开发</a>
                    
                </div>
            </div>
            
            
            <a class="navbar-item" title="GitHub" target="_blank" rel="noopener" href="https://github.com/wanzixin">
                
                <i class="fab fa-github"></i>
                
            </a>
               
            
        </div>
    </div>
</nav>

    <section class="section">
    <div class="container">
    <article class="article content gallery" itemscope itemprop="blogPost">
    <h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
        
            开发Web应用
        
    </h1>
    <div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
        <span class="column is-narrow">
            
                <span>7月 3 2021</span>
            
        </span>
        
        <span class="column is-narrow article-category">
            <i class="far fa-folder"></i>
            <a class="article-category-link" href="/zh-cn/categories/Study/">Study</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/">Java</a><span>></span><a class="article-category-link" href="/zh-cn/categories/Study/Java/Spring/">Spring</a>
        </span>
        
        
        <span class="column is-narrow">
            
            
            1 小时 读完 (约 11315 字)
        </span>
        
    </div>
    <div class="article-entry is-size-6-mobile" itemprop="articleBody">
    
        <html><head></head><body><p>在Web开发一章，我们学习了JavaEE中Web开发的基础：Servlet，总结来说：</p>
<ol>
<li>Servlet规范定义了几种标准组件：Servlet、JSP、Filter和Listener</li>
<li>Servlet的标准组件总是运行在Servlet容器中，如Tomcat、Jetty、WebLogic等</li>
</ol>
<p>直接使用Servlet进行Web开发好比直接在JDBC上操作数据库，比较繁琐。更好的方法是在Servlet基础上封装MVC框架，基于MVC开发Web应用，大部分时候，不需要接触Servlet API，省时省力。我们已经介绍过了如何编写MVC框架，当然自己写MVC主要是理解原理，要实现一个功能全面的MVC需要大量的工作和广泛的测试。所以，开发Web应用，首先要选择一个优秀的MVC框架。<span id="more"></span>Spring框架虽然可以集成任何Web框架，但是Spring本身开发的Spring MVC已经足够优秀，甚至已经不需要再集成其他MVC框架了。本章我们详细介绍如何基于Spring MVC开发Web应用。</p>
<h2 id="使用Spring-MVC"><a href="#使用Spring-MVC" class="headerlink" title="使用Spring MVC"></a>使用Spring MVC</h2><p>Servlet容器会为每个Web应用程序自动创建一个唯一的<code>ServletContext</code>实例，这个实例就代表了Web应用程序本身。如果直接使用Spring MVC，我们写出来的代码类似于：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{<br>    <span class="hljs-meta">@GetMapping("/register")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">register</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br><br>    <span class="hljs-meta">@PostMapping("/signin")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">signin</span><span class="hljs-params">(<span class="hljs-meta">@RequestParam("email")</span> String email, <span class="hljs-meta">@RequestParam("password")</span> String password)</span> </span>{<br>        ...<br>    }<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>但是，Spring提供的是一个IoC容器，所有的Bean，包括Controller，都在Spring IoC容器中被初始化。而Servlet容器由JavaEE服务器提供（如Tomcat），Servlet容器对IoC容器一无所知，它们之间依靠什么进行联系，又以何种顺序初始化？</p>
<p>在理解上述问题前，我们先把基于Spring MVC开发的项目结构搭起来，这个标准的Maven Web项目结构如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><code class="hljs ascii">spring-web-mvc<br>├── pom.xml<br>└── src<br>    └── main<br>        ├── java<br>        │   └── com<br>        │       └── itranswarp<br>        │           └── learnjava<br>        │               ├── AppConfig.java<br>        │               ├── DatabaseInitializer.java<br>        │               ├── entity<br>        │               │   └── User.java<br>        │               ├── service<br>        │               │   └── UserService.java<br>        │               └── web<br>        │                   └── UserController.java<br>        ├── resources<br>        │   ├── jdbc.properties<br>        │   └── logback.xml<br>        └── webapp<br>            ├── WEB-INF<br>            │   ├── templates<br>            │   │   ├── _base.html<br>            │   │   ├── index.html<br>            │   │   ├── profile.html<br>            │   │   ├── register.html<br>            │   │   └── signin.html<br>            │   └── web.xml<br>            └── static<br>                ├── css<br>                │   └── bootstrap.css<br>                └── js<br>                    └── jquery.js<br></code></pre></td></tr></tbody></table></figure>

<p>其中，<code>src/main/webapp</code>是标准Web目录，<code>WEB-INF</code>存放<code>web.xml</code>、编译的class、第三方jar，以及不允许浏览器直接访问的View模板和static目录（存放所有的静态文件）。</p>
<p>在<code>src/main/resources</code>目录存放的是Java程序读取的classpath资源文件，除了JDBC的配置文件<code>jdbc.properties</code>外，我们新增了一个<code>logback.xml</code>，这是Logback默认查找的配置文件。</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span><br>	<span class="hljs-tag">&lt;<span class="hljs-name">appender</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"STDOUT"</span></span><br><span class="hljs-tag">		<span class="hljs-attr">class</span>=<span class="hljs-string">"ch.qos.logback.core.ConsoleAppender"</span>&gt;</span><br>		<span class="hljs-tag">&lt;<span class="hljs-name">layout</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ch.qos.logback.classic.PatternLayout"</span>&gt;</span><br>			<span class="hljs-tag">&lt;<span class="hljs-name">Pattern</span>&gt;</span>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n<span class="hljs-tag">&lt;/<span class="hljs-name">Pattern</span>&gt;</span><br>		<span class="hljs-tag">&lt;/<span class="hljs-name">layout</span>&gt;</span><br>	<span class="hljs-tag">&lt;/<span class="hljs-name">appender</span>&gt;</span><br><br>	<span class="hljs-tag">&lt;<span class="hljs-name">logger</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"com.itranswarp.learnjava"</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"info"</span> <span class="hljs-attr">additivity</span>=<span class="hljs-string">"false"</span>&gt;</span><br>		<span class="hljs-tag">&lt;<span class="hljs-name">appender-ref</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"STDOUT"</span> /&gt;</span><br>	<span class="hljs-tag">&lt;/<span class="hljs-name">logger</span>&gt;</span><br><br>	<span class="hljs-tag">&lt;<span class="hljs-name">root</span> <span class="hljs-attr">level</span>=<span class="hljs-string">"info"</span>&gt;</span><br>		<span class="hljs-tag">&lt;<span class="hljs-name">appender-ref</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"STDOUT"</span> /&gt;</span><br>	<span class="hljs-tag">&lt;/<span class="hljs-name">root</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>上面给出了一个写入到标准输出的Logback配置，可以基于上述配置添加写入到文件的配置。</p>
<p>目录<code>src/main/java</code>就是我们编写Java代码的地方了。</p>
<h3 id="配置Spring-MVC"><a href="#配置Spring-MVC" class="headerlink" title="配置Spring MVC"></a>配置Spring MVC</h3><p>和普通Spring配置一样，编写正常的<code>AppConfig</code>后，只需加上<code>@EnableWebMvc</code>注解，就激活了Spring MVC。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration</span><br><span class="hljs-meta">@ComponentScan</span><br><span class="hljs-meta">@EnableWebMvc</span> <span class="hljs-comment">// 启用Spring MVC</span><br><span class="hljs-meta">@EnableTransactionManagement</span><br><span class="hljs-meta">@PropertySource("classpath:/jdbc.properties")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppConfig</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>除了创建DataSource、JdbcTemplate、PlatformTransactionManager外，AppConfig需要额外创建几个用于Spring MVC的Bean：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">WebMvcConfigurer <span class="hljs-title">createWebMvcConfigurer</span><span class="hljs-params">()</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> WebMvcConfigurer() {<br>        <span class="hljs-meta">@Override</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addResourceHandlers</span><span class="hljs-params">(ResourceHandlerRegistry registry)</span> </span>{<br>            registry.addResourceHandler(<span class="hljs-string">"/static/**"</span>).addResourceLocations(<span class="hljs-string">"/static/"</span>);<br>        }<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<p><code>WebMvcConfigurer</code>并不是必须的，但我们在这里创建一个默认的<code>WebMvcConfigurer</code>，只覆写<code>addResourceHandlers()</code>，目的是让Spring MVC自动处理静态文件，并且映射路径为<code>/static/**</code>。</p>
<p>另一个必须创建的Bean是<code>ViewResolver</code>，因为Spring MVC允许集成任何模板引擎，使用哪一个模板引擎就实例化一个对应的<code>ViewResolver</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">ViewResolver <span class="hljs-title">createViewResolver</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span> ServletContext servletContext)</span> </span>{<br>    PebbleEngine engine = <span class="hljs-keyword">new</span> PebbleEngine.Builder().autoEscaping(<span class="hljs-keyword">true</span>)<br>            .cacheActive(<span class="hljs-keyword">false</span>)<br>            .loader(<span class="hljs-keyword">new</span> ServletLoader(servletContext))<br>            .extension(<span class="hljs-keyword">new</span> SpringExtension())<br>            .build();<br>    PebbleViewResolver viewResolver = <span class="hljs-keyword">new</span> PebbleViewResolver();<br>    viewResolver.setPrefix(<span class="hljs-string">"/WEB-INF/templates/"</span>);<br>    viewResolver.setSuffix(<span class="hljs-string">""</span>);<br>    viewResolver.setPebbleEngine(engine);<br>    <span class="hljs-keyword">return</span> viewResolver;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p><code>ViewResolver</code>通过指定prefix和suffix来确定如何查找View。上述配置使用Pebble引擎，指定模板文件存放在<code>/WEB-INF/templates/</code>目录下。</p>
<p>剩下的Bean都是普通的<code>@Component</code>，但Controller必须被标记为<code>@Controller</code>。</p>
<p>如果是普通的Java应用程序，我们通过<code>main()</code>方法可以很简单的创建一个Spring容器实例：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{<br>    ApplicationContext context = <span class="hljs-keyword">new</span> AnnotationConfigApplicationContext(AppConfig.class);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>但现在是Web应用程序，而Web应用程序总是由Servlet容器创建，那么Springo容器应该由谁创建呢，在什么时候创建，Spring容器中的Controller又是如何通过Servlet调用的？</p>
<p>在Web应用中启动Spring容器有很多种方法，可以通过Listener启动，也可以通过Servlet启动，可以使用XML配置，也可以使用注解配置，这里我们介绍一种最简单的启动Spring容器的方式。</p>
<p>第一步，我们在<code>web.xml</code>中配置Spring MVC提供的<code>DispatcherServlet</code>：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">web-app</span> <span class="hljs-meta-keyword">PUBLIC</span></span><br><span class="hljs-meta"> <span class="hljs-meta-string">"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"</span></span><br><span class="hljs-meta"> <span class="hljs-meta-string">"http://java.sun.com/dtd/web-app_2_3.dtd"</span> &gt;</span><br><br><span class="hljs-tag">&lt;<span class="hljs-name">web-app</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">servlet</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-name</span>&gt;</span>dispatcher<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-class</span>&gt;</span>org.springframework.web.servlet.DispatcherServlet<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-class</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>contextClass<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>org.springframework.web.context.support.AnnotationConfigWebApplicationContext<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>contextConfigLocation<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>com.itranswarp.learnjava.AppConfig<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">load-on-startup</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">load-on-startup</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">servlet</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">servlet-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-name</span>&gt;</span>dispatcher<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/*<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">servlet-mapping</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">web-app</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>初始化参数<code>contextClass</code>指定使用注解配置的<code>AnnotationConfigWebApplicationContext</code>，配置文件的位置参数<code>contextConfigLocation</code>指向<code>AppConfig</code>的完整类名，最后把这个Servlet映射到<code>/*</code>，即处理所有URL。</p>
<p>有了这个配置，Servlet容器会首先初始化Spring MVC的<code>DispatcherServlet</code>，在<code>DispatcherServlet</code>启动时，它根据配置<code>AppConfig</code>创建了一个类型是WebApplicationContext的IoC容器，完成所有Bean的初始化，并将容器绑到<code>ServletContext</code>上。</p>
<p>因为<code>DispatcherServlet</code>持有IoC容器，能从IoC容器中获取所有<code>@Controller</code>的Bean，因此，DispatcherServlet接收到所有HTTP请求后，根据Controller方法配置的路径，就可以正确地把请求转发到指定方法，并根据返回的<code>ModelAndView</code>决定如何渲染页面。</p>
<p>最后，我们在AppConfig中通过<code>main()</code>启动嵌入式Tomcat：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>    Tomcat tomcat = <span class="hljs-keyword">new</span> Tomcat();<br>    tomcat.setPort(Integer.getInteger(<span class="hljs-string">"port"</span>, <span class="hljs-number">8080</span>));<br>    tomcat.getConnector();<br>    Context ctx = tomcat.addWebapp(<span class="hljs-string">""</span>, <span class="hljs-keyword">new</span> File(<span class="hljs-string">"src/main/webapp"</span>).getAbsolutePath());<br>    WebResourceRoot resources = <span class="hljs-keyword">new</span> StandardRoot(ctx);<br>    resources.addPreResources(<br>            <span class="hljs-keyword">new</span> DirResourceSet(resources, <span class="hljs-string">"/WEB-INF/classes"</span>, <span class="hljs-keyword">new</span> File(<span class="hljs-string">"target/classes"</span>).getAbsolutePath(), <span class="hljs-string">"/"</span>));<br>    ctx.setResources(resources);<br>    tomcat.start();<br>    tomcat.getServer().await();<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述Web应用程序就是我们使用Spring MVC时的一个最小启动功能集。</p>
<h3 id="编写Controller"><a href="#编写Controller" class="headerlink" title="编写Controller"></a>编写Controller</h3><p>有了Web应用程序的基本结构，我们的重点就可以放在如何编写Controller上了。Spring MVC对Controller没有固定要求，也不需要实现特定接口。以UserController为例，编写Controller只需要遵循以下要点。</p>
<p>总是标记<code>@Controller</code>，而不是<code>@Component</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>一个方法对应一个HTTP请求路径，用<code>@GetMapping</code>或<code>@PostMapping</code>表示GET或POST请求：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@PostMapping("/signin")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">doSignin</span><span class="hljs-params">(</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@RequestParam("email")</span> String email,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@RequestParam("password")</span> String password,</span></span><br><span class="hljs-params"><span class="hljs-function">        HttpSession session)</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>需要接收HTTP参数以<code>@RequestParam()</code>标注，可以设置默认值。如果方法参数需要传入<code>HttpServletRequest</code>、<code>HttpServletResponse</code>或者<code>HttpSession</code>，直接添加这个类型的参数即可，Spring MVC会自动按类型传入。</p>
<p>返回的ModelAndView通常包含View的路径和一个Map作为Model，但也可以没有Model，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ModelAndView(<span class="hljs-string">"signin.html"</span>); <span class="hljs-comment">// 仅View，没有Model</span><br></code></pre></td></tr></tbody></table></figure>

<p>返回重定向时既可以写<code>new ModelAndView("redirect:/signin")</code>，也可以直接返回String：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">index</span><span class="hljs-params">()</span> </span>{<br>    <span class="hljs-keyword">if</span> (...) {<br>        <span class="hljs-keyword">return</span> <span class="hljs-string">"redirect:/signin"</span>;<br>    } <span class="hljs-keyword">else</span> {<br>        <span class="hljs-keyword">return</span> <span class="hljs-string">"redirect:/profile"</span>;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果在方法内部直接操作HttpServletResponse发送响应，返回null表示无需进一步处理：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">download</span><span class="hljs-params">(HttpServletResponse response)</span> </span>{<br>    <span class="hljs-keyword">byte</span>[] data = ...<br>    response.setContentType(<span class="hljs-string">"application/octet-stream"</span>);<br>    OutputStream output = response.getOutputStream();<br>    output.write(data);<br>    output.flush();<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>对URL进行分组，每组对应一个Controller是很好的组织形式，并可以在Controller的class定义出添加URL前缀，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-meta">@RequestMapping("/user")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{<br>    <span class="hljs-comment">// 注意实际URL映射是/user/profile</span><br>    <span class="hljs-meta">@GetMapping("/profile")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">profile</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br><br>    <span class="hljs-comment">// 注意实际URL映射是/user/changePassword</span><br>    <span class="hljs-meta">@GetMapping("/changePassword")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">changePassword</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>实际方法的URL映射总是前缀+路径，这种形式还可以有效避免不小心导致的重复URL映射。</p>
<p>可见，Spring MVC帮助我们编写既简单又灵活的Controller实现。</p>
<h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>使用Spring MVC时，整个Web应用程序按如下顺序启动：</p>
<ol>
<li>启动Tomcat服务器</li>
<li>Tomcat读取web.xml并初始化DispatcherServlet</li>
<li>DispatcherServlet创建IoC容器并自动注册到ServletContext中</li>
</ol>
<p>启动后，浏览器发送的HTTP请求全部由DispathcerServlet接收，并根据配置转发到指定的Controller的指定方法处理。</p>
<h2 id="使用REST"><a href="#使用REST" class="headerlink" title="使用REST"></a>使用REST</h2><p>使用Spring MVC开发Web应用程序的主要工作就是编写Controller。在Web应用中，除了需要使用MVC给用户显示页面外，还有一类API接口，我们称之为REST，通常输入输出都是JSON，便于第三方调用或者使用页面JavaScript与之交互。</p>
<p>直接在Controller中处理JSON是可以的，因为Spring MVC的<code>@GetMapping</code>和<code>@PostMapping</code>都支持指定输入和输出的格式。如果我们想接收JSON，输出JSON，可以这样写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@PostMapping(value = "/rest",</span><br><span class="hljs-meta">             consumes = "application/json;charset=UTF-8",</span><br><span class="hljs-meta">             produces = "application/json;charset=UTF-8")</span><br><span class="hljs-meta">@ResponseBody</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">rest</span><span class="hljs-params">(<span class="hljs-meta">@RequestBody</span> User user)</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-string">"{\"restSupport\":true}"</span>;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>对应的Maven功程需要引入Jackson这个依赖。注意到<code>@PostMapping</code>使用<code>consumes</code>声明能接收的类型，使用<code>produces</code>声明输出的类型，并且额外加了<code>@ResponseBody</code>表示返回的<code>String</code>无需额外处理，直接作为输出内容写入<code>HttpServletResponse</code>。输入的JSON则根据注解<code>@RequestBody</code>直接被Spring反序列化为<code>User</code>这个Bean。</p>
<p>直接用Spring的Controller配合一大堆注解写REST太麻烦了，因此，Spring还额外提供了一个<code>@RestController</code>注解。使用<code>@RestController</code>替代<code>@Controller</code>后，每个方法自动变成API接口方法，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@RestController</span><br><span class="hljs-meta">@RequestMapping("/api")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiController</span> </span>{<br>    <span class="hljs-meta">@Autowired</span><br>    UserService userService;<br><br>    <span class="hljs-meta">@GetMapping("/users")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;User&gt; <span class="hljs-title">users</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">return</span> userService.getUsers();<br>    }<br><br>    <span class="hljs-meta">@GetMapping("/users/{id}")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> User <span class="hljs-title">user</span><span class="hljs-params">(<span class="hljs-meta">@PathVariable("id")</span> <span class="hljs-keyword">long</span> id)</span> </span>{<br>        <span class="hljs-keyword">return</span> userService.getUserById(id);<br>    }<br><br>    <span class="hljs-meta">@PostMapping("/signin")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> Map&lt;String, Object&gt; <span class="hljs-title">signin</span><span class="hljs-params">(<span class="hljs-meta">@RequestBody</span> SignInRequest signinRequest)</span> </span>{<br>        <span class="hljs-keyword">try</span> {<br>            User user = userService.signin(signinRequest.email, signinRequest.password);<br>            <span class="hljs-keyword">return</span> Map.of(<span class="hljs-string">"user"</span>, user);<br>        } <span class="hljs-keyword">catch</span> (Exception e) {<br>            <span class="hljs-keyword">return</span> Map.of(<span class="hljs-string">"error"</span>, <span class="hljs-string">"SIGNIN_FAILED"</span>, <span class="hljs-string">"message"</span>, e.getMessage());<br>        }<br>    }<br><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignInRequest</span> </span>{<br>        <span class="hljs-keyword">public</span> String email;<br>        <span class="hljs-keyword">public</span> String password;<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>编写REST接口只需要定义<code>@RestController</code>，然后，每个方法都是一个API接口，输入和输出只要能被Jackson序列化或反序列化为JSON。我们用浏览器测试GET请求，可直接显示JSON响应。</p>
<p>使用curl命令可以测试到JSON的输出，User能被正确序列化为JSON，但暴露了password属性，这是我们不愿意的。要避免输出password属性，可以把User复制到另一个UserBean对象，该对象只持有必要的属性，但这样做比较繁琐。另一种简单的方法是直接在User的password属性定义处加上<code>@JsonIgnore</code>表示完全忽略该属性。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{<br>    ...<br><br>    <span class="hljs-meta">@JsonIgnore</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getPassword</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">return</span> password;<br>    }<br><br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>但是这样一来，如果写一个<code>register(User user)</code>方法，那么该方法的User对象也拿不到注册时用户传入的密码了。如果要允许输入password，但不允许输出password，即在JSON序列化和反序列化时，允许写属性，禁用读属性，可以更精细地控制如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> </span>{<br>    ...<br><br>    <span class="hljs-meta">@JsonProperty(access = Access.WRITE_ONLY)</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">getPassword</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-keyword">return</span> password;<br>    }<br><br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>同样地，可以使用<code>@JsonProperty(access = Access.READ_ONLY)</code>允许输出，不允许输入。</p>
<h2 id="集成Filter"><a href="#集成Filter" class="headerlink" title="集成Filter"></a>集成Filter</h2><p>在Servlet规范中，我们还可以使用Filter，如果要在Spring MVC中使用Filter，应该怎么做？</p>
<p>有同学可能发现了，Servlet默认按非UTF-8编码读取参数，为了修复这一问题，我们可以用一个EndoingFilter，在全局范围类给<code>HttpServletRequest</code>和<code>HttpServletResponse</code>强制设置为UTF-8。可以自己编写一个EncodingFilter，也可以直接使用Spring MVC自带的一个<code>CharacterEncodingFilter</code>。配置Filter时，只需在<code>web.xml</code>中声明即可：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">web-app</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>encodingFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-class</span>&gt;</span>org.springframework.web.filter.CharacterEncodingFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-class</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>encoding<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>UTF-8<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>forceEncoding<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>encodingFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/*<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter-mapping</span>&gt;</span><br>    ...<br><span class="hljs-tag">&lt;/<span class="hljs-name">web-app</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>因为这种Filter和我们业务关系不大，注意到CharacterEncodingFilter其实和Spring的IoC容器没有任何关系，两者均不知道互相的存在，所以配置Filter非常简单。</p>
<p>我们再考虑这样一个问题：如果允许用户使用Basic模式进行用户验证，即在HTTP请求中添加头<code>Authorization: Basic email:password</code>，这个需求如何实现？</p>
<p>编写一个AuthFilter是最简单的实现方式：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthFilter</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-meta">@Autowired</span><br>    UserService userService;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain chain)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> IOException, ServletException </span>{<br>        HttpServletRequest req = (HttpServletRequest) request;<br>        <span class="hljs-comment">// 获取Authorization头:</span><br>        String authHeader = req.getHeader(<span class="hljs-string">"Authorization"</span>);<br>        <span class="hljs-keyword">if</span> (authHeader != <span class="hljs-keyword">null</span> &amp;&amp; authHeader.startsWith(<span class="hljs-string">"Basic "</span>)) {<br>            <span class="hljs-comment">// 从Header中提取email和password:</span><br>            String email = prefixFrom(authHeader);<br>            String password = suffixFrom(authHeader);<br>            <span class="hljs-comment">// 登录:</span><br>            User user = userService.signin(email, password);<br>            <span class="hljs-comment">// 放入Session:</span><br>            req.getSession().setAttribute(UserController.KEY_USER, user);<br>        }<br>        <span class="hljs-comment">// 继续处理请求:</span><br>        chain.doFilter(request, response);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在Spring中创建的这个<code>AuthFilter</code>是一个普通Bean，Servlet容器并不知道，所以它不起作用。如果我们直接在<code>web.xml</code>中声明这个<code>AuthFilter</code>，但<code>AuthFilter</code>的实例是有Servlet容器而不是Spring容器（也就是IoC容器）初始化，因此<code>@AutoWired</code>不生效，用于登录的<code>UserService</code>成员变量将永远是<code>null</code>。</p>
<p>所以，得通过一种方式，让Servlet容器实例化的Filter，间接引用Spring容器实例化的<code>AuthFilter</code>。Spring MVC提供了一个<code>DelegatingFilterProxy</code>来做这件事：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">web-app</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>authFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-class</span>&gt;</span>org.springframework.web.filter.DelegatingFilterProxy<span class="hljs-tag">&lt;/<span class="hljs-name">filter-class</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>authFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/*<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter-mapping</span>&gt;</span><br>    ...<br><span class="hljs-tag">&lt;/<span class="hljs-name">web-app</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>我们来看实现原理：</p>
<ol>
<li>Servlet容器从<code>web.xml</code>读取配置，实例化<code>DelegatingFilterProxy</code>，注意命名是<code>authFilter</code></li>
<li>Spring容器扫描<code>@Component</code>实例化AuthFilter</li>
</ol>
<p>当<code>DelegatingFilterProxy</code>生效后，它会自动查找注册在ServletContext上的Spring容器，再试图从容器中查找名为<code>authFilter</code>的Bean，也就是我们用<code>@Component</code>声明的<code>AuthFilter</code>。</p>
<p><code>DelegatingFilterProxy</code>将请求代理给<code>AuthFilter</code>，核心代码如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DelegatingFilterProxy</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Filter</span> </span>{<br>    <span class="hljs-keyword">private</span> Filter delegate;<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">doFilter</span><span class="hljs-params">(...)</span> <span class="hljs-keyword">throws</span> ... </span>{<br>        <span class="hljs-keyword">if</span> (delegate == <span class="hljs-keyword">null</span>) {<br>            delegate = findBeanFromSpringContainer();<br>        }<br>        delegate.doFilter(req, resp, chain);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这是一个代理模式的简单应用，我们用图表表示它们之间的引用关系如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs ascii">┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─<br>  ┌─────────────────────┐        ┌───────────┐   │<br>│ │DelegatingFilterProxy│─│─│─ ─&gt;│AuthFilter │<br>  └─────────────────────┘        └───────────┘   │<br>│ ┌─────────────────────┐ │ │    ┌───────────┐<br>  │  DispatcherServlet  │─ ─ ─ ─&gt;│Controllers│   │<br>│ └─────────────────────┘ │ │    └───────────┘<br>                                                 │<br>│    Servlet Container    │ │  Spring Container<br> ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘<br></code></pre></td></tr></tbody></table></figure>

<p>如果在web.xml中配置的Filter名称和Spring容器的Bean的名字不一致，那么需要指定Bean的名字：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">filter</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>basicAuthFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-class</span>&gt;</span>org.springframework.web.filter.DelegatingFilterProxy<span class="hljs-tag">&lt;/<span class="hljs-name">filter-class</span>&gt;</span><br>    <span class="hljs-comment">&lt;!-- 指定Bean的名字 --&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>targetBeanName<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>authFilter<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">filter</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>实际应用时，尽量保持名字一致，以减少不必要的配置。</p>
<blockquote>
<p>注意：Basic认证模式并不安全，本节只用来作为使用Filter的示例。</p>
</blockquote>
<h2 id="使用Interceptor"><a href="#使用Interceptor" class="headerlink" title="使用Interceptor"></a>使用Interceptor</h2><p>在Web应用程序中，注意到使用Filter时，Filter由Servlet容器管理，它在Spring MVC的Web应用程序中作用范围如下：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs ascii">         │   ▲<br>         ▼   │<br>       ┌───────┐<br>       │Filter1│<br>       └───────┘<br>         │   ▲<br>         ▼   │<br>       ┌───────┐<br>┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐<br>       └───────┘<br>│        │   ▲                  │<br>         ▼   │<br>│ ┌─────────────────┐           │<br>  │DispatcherServlet│&lt;───┐<br>│ └─────────────────┘    │      │<br>   │              ┌────────────┐<br>│  │              │ModelAndView││<br>   │              └────────────┘<br>│  │                     ▲      │<br>   │    ┌───────────┐    │<br>│  ├───&gt;│Controller1│────┤      │<br>   │    └───────────┘    │<br>│  │                     │      │<br>   │    ┌───────────┐    │<br>│  └───&gt;│Controller2│────┘      │<br>        └───────────┘<br>└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘<br></code></pre></td></tr></tbody></table></figure>

<p>上图虚线框就是Filter2的拦截范围，Filter实际上不知道后续处理是通过Spring MVC提供的<code>DispathcerServlet</code>还是其他Servlet组件，因为Filter是Servlet规范定义的标准组件，它可以应用在任何基于Servlet的程序中。</p>
<p>如果只基于Spring MVC开发应用程序，还可以使用Spring MVC提供的一种功能类似Filter的拦截器：Interceptor。和Filter相比，Interceptor拦截范围不是后续整个处理流程，而是仅针对Controller的拦截。</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs ascii">       │   ▲<br>       ▼   │<br>     ┌───────┐<br>     │Filter1│<br>     └───────┘<br>       │   ▲<br>       ▼   │<br>     ┌───────┐<br>     │Filter2│<br>     └───────┘<br>       │   ▲<br>       ▼   │<br>┌─────────────────┐<br>│DispatcherServlet│&lt;───┐<br>└─────────────────┘    │<br> │              ┌────────────┐<br> │              │ModelAndView│<br> │              └────────────┘<br> │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ▲<br> │    ┌───────────┐    │<br> ├─┼─&gt;│Controller1│──┼─┤<br> │    └───────────┘    │<br> │ │                 │ │<br> │    ┌───────────┐    │<br> └─┼─&gt;│Controller2│──┼─┘<br>      └───────────┘<br>   └ ─ ─ ─ ─ ─ ─ ─ ─ ┘<br></code></pre></td></tr></tbody></table></figure>

<p>上图虚线框就是Interceptor的拦截范围。注意到Controller的处理方法一般都类似这样：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Controller1</span> </span>{<br>    <span class="hljs-meta">@GetMapping("/path/to/hello")</span><br>    <span class="hljs-function">ModelAndView <span class="hljs-title">hello</span><span class="hljs-params">()</span> </span>{<br>        ...<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>所以，Interceptor的拦截范围其实就是Controller方法，实际上相当于基于AOP的方法拦截。因为Interceptor只拦截Controller方法，所以要注意，返回ModelAndView后，后续对View的渲染就脱离了Interceptor的拦截范围。</p>
<p>使用Interceptor的好处是Interceptor本身是Spring管理的Bean，因此注入任意的Bean都非常简单，此外可以应用多个Interceptor，并通过简单的<code>@Order</code>指定顺序。</p>
<p>我们先写一个LoggerInterceptor：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Order(1)</span><br><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoggerInterceptor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">HandlerInterceptor</span> </span>{<br><br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">preHandle</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        logger.info(<span class="hljs-string">"preHandle {}..."</span>, request.getRequestURI());<br>        <span class="hljs-keyword">if</span> (request.getParameter(<span class="hljs-string">"debug"</span>) != <span class="hljs-keyword">null</span>) {<br>            PrintWriter pw = response.getWriter();<br>            pw.write(<span class="hljs-string">"&lt;p&gt;DEBUG MODE&lt;/p&gt;"</span>);<br>            pw.flush();<br>            <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;<br>        }<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;<br>    }<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">postHandle</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        logger.info(<span class="hljs-string">"postHandle {}."</span>, request.getRequestURI());<br>        <span class="hljs-keyword">if</span> (modelAndView != <span class="hljs-keyword">null</span>) {<br>            modelAndView.addObject(<span class="hljs-string">"__time__"</span>, LocalDateTime.now());<br>        }<br>    }<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterCompletion</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        logger.info(<span class="hljs-string">"afterCompletion {}: exception = {}"</span>, request.getRequestURI(), ex);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>一个Interceptor必须实现HandlerInterceptor接口，可以选择实现<code>preHandle()</code>、<code>postHandle()</code>和<code>afterCompletion()</code>方法。<code>preHandle()</code>是Controller方法调用前执行，<code>postHandle()</code>是Controller方法争藏返回后执行，而<code>afterCompletion()</code>无论Controller方法是否抛异常都会执行，参数<code>ex</code>就是Controller方法抛出的异常（未抛出异常是<code>null</code>）。</p>
<p>在<code>preHandle()</code>中也可以直接处理响应，然后返回false标识无需调用Controller方法继续处理了，通常在认证或者安全检查失败时直接返回错误响应。在<code>postHandle()</code>中，因为捕获了Controller方法返回的<code>ModelAndView</code>，所以可以继续在<code>ModelAndView</code>中添加一些通用数据，很多页面需要的全局数据如CopyRight信息都可以放到这里，无需在每个Controller方法中重复添加。</p>
<p>我们添加一个<code>AuthInterceptor</code>，用于替代上一节使用的<code>AuthFilter</code>进行Basic认证的功能：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Order(2)</span><br><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthInterceptor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">HandlerInterceptor</span> </span>{<br><br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Autowired</span><br>    UserService userService;<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">preHandle</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span></span><br><span class="hljs-function">            <span class="hljs-keyword">throws</span> Exception </span>{<br>        logger.info(<span class="hljs-string">"pre authenticate {}..."</span>, request.getRequestURI());<br>        <span class="hljs-keyword">try</span> {<br>            authenticateByHeader(request);<br>        } <span class="hljs-keyword">catch</span> (RuntimeException e) {<br>            logger.warn(<span class="hljs-string">"login by authorization header failed."</span>, e);<br>        }<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;<br>    }<br><br>    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">authenticateByHeader</span><span class="hljs-params">(HttpServletRequest req)</span> </span>{<br>        String authHeader = req.getHeader(<span class="hljs-string">"Authorization"</span>);<br>        <span class="hljs-keyword">if</span> (authHeader != <span class="hljs-keyword">null</span> &amp;&amp; authHeader.startsWith(<span class="hljs-string">"Basic "</span>)) {<br>            logger.info(<span class="hljs-string">"try authenticate by authorization header..."</span>);<br>            String up = <span class="hljs-keyword">new</span> String(Base64.getDecoder().decode(authHeader.substring(<span class="hljs-number">6</span>)), StandardCharsets.UTF_8);<br>            <span class="hljs-keyword">int</span> pos = up.indexOf(<span class="hljs-string">':'</span>);<br>            <span class="hljs-keyword">if</span> (pos &gt; <span class="hljs-number">0</span>) {<br>                String email = URLDecoder.decode(up.substring(<span class="hljs-number">0</span>, pos), StandardCharsets.UTF_8);<br>                String password = URLDecoder.decode(up.substring(pos + <span class="hljs-number">1</span>), StandardCharsets.UTF_8);<br>                User user = userService.signin(email, password);<br>                req.getSession().setAttribute(UserController.KEY_USER, user);<br>                logger.info(<span class="hljs-string">"user {} login by authorization header ok."</span>, email);<br>            }<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这个<code>AuthInterceptor</code>是由Spring容器直接管理的，因此注入<code>UserService</code>非常方便。</p>
<p>最后，要让拦截器生效，我们在<code>WebMvcConfigurer</code>中注册所有的Interceptor：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">WebMvcConfigurer <span class="hljs-title">createWebMvcConfigurer</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span> HandlerInterceptor[] interceptors)</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> WebMvcConfigurer() {<br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addInterceptors</span><span class="hljs-params">(InterceptorRegistry registry)</span> </span>{<br>            <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> interceptor : interceptors) {<br>                registry.addInterceptor(interceptor);<br>            }<br>        }<br>        ...<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<blockquote>
<p>如果拦截器没有生效，请检查是否忘记了在WebMvcConfigurer中注册。</p>
</blockquote>
<h3 id="处理异常"><a href="#处理异常" class="headerlink" title="处理异常"></a>处理异常</h3><p>在Controller中，Spring MVC还允许定义基于<code>@ExceptionHandler</code>注解的异常处理方法，我们来看示例代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserController</span> </span>{<br>    <span class="hljs-meta">@ExceptionHandler(RuntimeException.class)</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> ModelAndView <span class="hljs-title">handleUnknowException</span><span class="hljs-params">(Exception ex)</span> </span>{<br>        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ModelAndView(<span class="hljs-string">"500.html"</span>, Map.of(<span class="hljs-string">"error"</span>, ex.getClass().getSimpleName(), <span class="hljs-string">"message"</span>, ex.getMessage()));<br>    }<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>异常处理方法没有固定的方法签名，可以传入<code>Exception</code>、<code>HttpServletRequest</code>等，返回值可以是<code>void</code>，也可以是<code>ModelAndView</code>，上述代码通过<code>@ExceptionHandler(RuntimeException.class)</code>表示当发生<code>RuntimeException</code>的时候，就自动调用此方法处理。</p>
<p>注意到我们返回了一个新的<code>ModelAndView</code>，这样在应用程序内部发生了预料之外的异常，可以给用户显示一个出错页面，而不是简单的500 Internal Server Error或404 Not Found。</p>
<p>可以编写多个错误处理方法，每个方法针对特定的异常，例如，处理<code>LoginException</code>使得页面可以自动跳转到登录页。使用<code>ExceptionHandler</code>时，要注意它仅作用于当前的Controller，即ControllerA中定义的ExceptionHander方法对ControllerB不起作用。那如果我们有很多Controller，每个Controller都需要处理一些通用异常，应该怎么避免重复代码？</p>
<h2 id="处理CORS"><a href="#处理CORS" class="headerlink" title="处理CORS"></a>处理CORS</h2><p>在开发REST应用时，很多时候是通过页面的JavaScript和后端的REST API交互。在JavaScript与REST API交互的时候，有很多安全限制。默认情况下，浏览器按同源策略放行JavaScript调用API，即：</p>
<ul>
<li>如果A站在域名<code>a.com</code>页面的JavaScript调用A站自己的API时，没有问题</li>
<li>如果A站在域名<code>a.com</code>页面的JavaScript调用B站<code>b.com</code>的API时，将被浏览器拒绝访问，因为不满足同源策略</li>
</ul>
<p>同源要求域名完全相同（<code>a.com</code>和<code>www.a.com</code>不同），协议要相同（<code>http</code>和<code>https</code>不同），端口要相同。</p>
<p>那么，在域名<code>a.com</code>页面的JavaScript要调用<code>b.com</code>的API时，还有没有办法？</p>
<p>有的，那就是CORS，全称叫Cross-Origin Resource Sharing，是HTML5规范定义的如何跨域访问资源。如果A站的JavaScript访问B站API时，B站能够返回响应头<code>Access-Control-Allow-Origin: http://a.com</code>，那么浏览器就允许A站的JavaScript访问B站的API。注意到跨域访问能否成功，取决于B站是否愿意给A站返回一个正确的<code>Access-Control-Allow-Origin</code>响应头，所以决定权永远在提供API的服务方手中。</p>
<p>使用Spring的<code>@RestController</code>开发REST应用时，同样会面对跨域问题。如果我们允许指定的网站通过页面JavaScript访问这些REST API，就必须正确地设置CORS。我们一一介绍设置CORS的几种方法。</p>
<h3 id="使用-CrossOrigin"><a href="#使用-CrossOrigin" class="headerlink" title="使用@CrossOrigin"></a>使用@CrossOrigin</h3><p>在<code>@RestController</code>的class级别或method级别定义一个<code>@CrossOrigin</code>，例如：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@CrossOrigin(origins = "http://local.liaoxuefeng.com:8080")</span><br><span class="hljs-meta">@RestController</span><br><span class="hljs-meta">@RequestMapping("/api")</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApiController</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>上述定义在<code>ApiController</code>处的<code>@CrossOrigin</code>指定了只允许来自<code>local.liaoxuefeng.com</code>跨域访问，允许多个域名访问需要写成数组形式，例如<code>origins = {"http://a.com", "https://www.b.com"}</code>。如果允许任何域名访问，写成<code>origins = "*"</code>即可。</p>
<p>如果有多个REST Controller都需要使用CORS，那么每个Controller都必须标注<code>@CrossOrigin</code>注解。</p>
<h3 id="使用CorsRegistry"><a href="#使用CorsRegistry" class="headerlink" title="使用CorsRegistry"></a>使用CorsRegistry</h3><p>在<code>WebMvcConfigurer</code>中定义一个全局CORS配置，下面是一个示例：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">WebMvcConfigurer <span class="hljs-title">createWebMvcConfigurer</span><span class="hljs-params">()</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> WebMvcConfigurer() {<br>        <span class="hljs-meta">@Override</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addCorsMappings</span><span class="hljs-params">(CorsRegistry registry)</span> </span>{<br>            registry.addMapping(<span class="hljs-string">"/api/**"</span>)<br>                    .allowedOrigins(<span class="hljs-string">"http://local.liaoxuefeng.com:8080"</span>)<br>                    .allowedMethods(<span class="hljs-string">"GET"</span>, <span class="hljs-string">"POST"</span>)<br>                    .maxAge(<span class="hljs-number">3600</span>);<br>            <span class="hljs-comment">// 可以继续添加其他URL规则:</span><br>            <span class="hljs-comment">// registry.addMapping("/rest/v2/**")...</span><br>        }<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这种方式可以创建一个全局CORS配置，如果仔细地设计URL结构，那么可以一目了然地看到各个URL的CORS规则，推荐使用这种方式配置CORS。</p>
<h3 id="使用CorsFilter"><a href="#使用CorsFilter" class="headerlink" title="使用CorsFilter"></a>使用CorsFilter</h3><p>使用Spring提供的CorsFilter，我们在[集成Filter中详细介绍了将Spring容器内置的Bean暴露为Servlet容器的Filter的方法，由于这种配置方式需要修改<code>web.xml</code>，也比较繁琐，所以推荐使用第二种方式。</p>
<h2 id="国际化"><a href="#国际化" class="headerlink" title="国际化"></a>国际化</h2><p>在开发应用程序时，经常会遇到支持多语言的需求，这种支持多语言的功能称之为国际化，英文名是internationalization，缩写为i18n（因为首字母i和末字母n之间有18个字母）。还有针对特定地区的本地化功能，英文是localization，缩写为l10n，本地化是指根据地区调整类似姓名、日期的显示等。也有把上述两者合称为全球化，英文是globalization，缩写为g11n。</p>
<p>在Java中，支持多语言和本地化是通过<code>MessageFormat</code>配合<code>Locale</code>实现的。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">import</span> java.text.MessageFormat;<br><span class="hljs-keyword">import</span> java.util.Locale;<br><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Time</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{<br>        <span class="hljs-keyword">double</span> price = <span class="hljs-number">123.5</span>;<br>        <span class="hljs-keyword">int</span> number = <span class="hljs-number">10</span>;<br>        Object[] arguments = { price, number };<br>        MessageFormat mfUS = <span class="hljs-keyword">new</span> MessageFormat(<span class="hljs-string">"Pay {0,number,currency} for {1} books."</span>, Locale.US); <span class="hljs-comment">// Pay $123.50 for 10 books.</span><br>        System.out.println(mfUS.format(arguments));<br>        MessageFormat mfZH = <span class="hljs-keyword">new</span> MessageFormat(<span class="hljs-string">"{1}本书一共{0,number,currency}。"</span>, Locale.CHINA); <span class="hljs-comment">// 10本书一共¥123.50。</span><br>        System.out.println(mfZH.format(arguments));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>对于Web应用程序，要实现国际化功能，主要是渲染View的时候，要把各种语言的资源文件提出来，这样，不同的用户访问同一个页面时，显示的语言就是不同的。</p>
<p>我们来看看在Spring MVC应用程序中如何实现国际货。</p>
<h3 id="获取Locale"><a href="#获取Locale" class="headerlink" title="获取Locale"></a>获取Locale</h3><p>第一步获取用户的Locale。在Web应用程序中，HTTP规范规定了浏览器会在请求中携带<code>Accept-Language</code>头，用来指示用户浏览器设定的语言顺序，如：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">Accept-Language: zh-CN,zh;q=0.8,en;q=0.2<br></code></pre></td></tr></tbody></table></figure>

<p>上述HTTP请求头表示优先选择简体中文，其次选择中文，最后选择英文。<code>q</code>表示权重，解析后我们可获得一个根据优先级排序的语言列表，把它转换为Java的<code>Locale</code>，即获得了用户的<code>Locale</code>。大多数框架通常只返回权重最高的<code>Locale</code>。</p>
<p>Spring MVC通过<code>LocaleResolver</code>来自动从<code>HttpServletRequest</code>中获取<code>Locale</code>。有多种<code>LocaleResolver</code>的实现类，其中最常用的是<code>CookieLocaleResolver</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">LocaleResolver <span class="hljs-title">createLocaleResolver</span><span class="hljs-params">()</span> </span>{<br>    <span class="hljs-keyword">var</span> clr = <span class="hljs-keyword">new</span> CookieLocaleResolver();<br>    clr.setDefaultLocale(Locale.ENGLISH);<br>    clr.setDefaultTimeZone(TimeZone.getDefault());<br>    <span class="hljs-keyword">return</span> clr;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p><code>CookieLocaleResolver</code>从<code>HttpServletRequest</code>中获取<code>Locale</code>时，首先根据一个特定的Cookie判断是否指定了<code>Locale</code>，如果没有，就从HTTP头获取，如果还没有，就返回默认的<code>Locale</code>。</p>
<p>当用户第一次访问网站时，<code>CookieLocaleResolver</code>只能从HTTP头获取<code>Locale</code>，即使用浏览器的默认语言。通常网站也允许用户自己选择语言，此时，<code>CookieLocaleResolver</code>就会把用户选择的语言存放到Cookie中，下一次访问时，就会返回用户上次选择的语言而不是浏览器默认语言。</p>
<h3 id="提取资源文件"><a href="#提取资源文件" class="headerlink" title="提取资源文件"></a>提取资源文件</h3><p>第二步是把写死在模板中的字符串以资源文件的形式存储在外部。</p>
<p>对于多语言，主文件名如果命名为<code>messages</code>，那么资源文件必须按如下方式命名并放入classpath中：</p>
<ul>
<li>默认语言，文件名必须为<code>messages.properties</code></li>
<li>简体中文，Locale是<code>zh_CN</code>，文件名必须为<code>messages_zh_CN.properties</code></li>
<li>日文，Locale是<code>ja_JP</code>，文件名必须为<code>messages_ja_JP.properties</code></li>
<li>其它更多语言……</li>
</ul>
<p>每个资源文件都有相同的key，例如，默认语言是英文，文件<code>messages.properties</code>内容如下：</p>
<figure class="highlight properties hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs properties"><span class="hljs-meta">language.select</span>=<span class="hljs-string">Language</span><br><span class="hljs-attr">home</span>=<span class="hljs-string">Home</span><br><span class="hljs-attr">signin</span>=<span class="hljs-string">Sign In</span><br><span class="hljs-attr">copyright</span>=<span class="hljs-string">Copyright©{0,number,#}</span><br></code></pre></td></tr></tbody></table></figure>

<p>文件<code>messages_zh_CN.properties</code>内容如下：</p>
<figure class="highlight properties hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs properties"><span class="hljs-meta">language.select</span>=<span class="hljs-string">语言</span><br><span class="hljs-attr">home</span>=<span class="hljs-string">首页</span><br><span class="hljs-attr">signin</span>=<span class="hljs-string">登录</span><br><span class="hljs-attr">copyright</span>=<span class="hljs-string">版权所有©{0,number,#}</span><br></code></pre></td></tr></tbody></table></figure>

<h3 id="创建MessageSource"><a href="#创建MessageSource" class="headerlink" title="创建MessageSource"></a>创建MessageSource</h3><p>第三步是创建一个Spring提供的<code>MessageSource</code>实例，它自动读取所有的<code>.properties</code>文件，并提供一个统一接口来实现“翻译”：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// code, arguments, locale:</span><br>String text = messageSource.getMessage(<span class="hljs-string">"signin"</span>, <span class="hljs-keyword">null</span>, locale);<br></code></pre></td></tr></tbody></table></figure>

<p>其中，<code>signin</code>是我们在<code>.properties</code>文件中定义的key，第二个参数是<code>Object[]</code>数组作为格式化时传入的参数，最后一个参数就是获取的用户<code>Locale</code>实例。</p>
<p>创建<code>MessageSource</code>如下：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean("i18n")</span><br><span class="hljs-function">MessageSource <span class="hljs-title">createMessageSource</span><span class="hljs-params">()</span> </span>{<br>    <span class="hljs-keyword">var</span> messageSource = <span class="hljs-keyword">new</span> ResourceBundleMessageSource();<br>    <span class="hljs-comment">// 指定文件是UTF-8编码:</span><br>    messageSource.setDefaultEncoding(<span class="hljs-string">"UTF-8"</span>);<br>    <span class="hljs-comment">// 指定主文件名:</span><br>    messageSource.setBasename(<span class="hljs-string">"messages"</span>);<br>    <span class="hljs-keyword">return</span> messageSource;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>注意到<code>ResourceBundleMessageSource</code>会自动根据主文件名自动把所有相关语言的资源文件都读进来。</p>
<p>再注意到Spring容器会创建不只一个<code>MessageSource</code>实例，我们自己创建的这个<code>MessageSource</code>是专门给页面国际化使用的，因此命名为<code>i18n</code>，不会与其它<code>MessageSource</code>实例冲突。</p>
<h3 id="实现多语言"><a href="#实现多语言" class="headerlink" title="实现多语言"></a>实现多语言</h3><p>要在View中实现MessageSource加上Locale输出多语言，我们编写一个Interceptor，把相关资源注入到ModelAndView中：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MvcInterceptor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">HandlerInterceptor</span> </span>{<br>    <span class="hljs-meta">@Autowired</span><br>    LocaleResolver localeResolver;<br><br>    <span class="hljs-comment">// 注意注入的MessageSource名称是i18n:</span><br>    <span class="hljs-meta">@Autowired</span><br>    <span class="hljs-meta">@Qualifier("i18n")</span><br>    MessageSource messageSource;<br><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">postHandle</span><span class="hljs-params">(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        <span class="hljs-keyword">if</span> (modelAndView != <span class="hljs-keyword">null</span>) {<br>            <span class="hljs-comment">// 解析用户的Locale:</span><br>            Locale locale = localeResolver.resolveLocale(request);<br>            <span class="hljs-comment">// 放入Model:</span><br>            modelAndView.addObject(<span class="hljs-string">"__messageSource__"</span>, messageSource);<br>            modelAndView.addObject(<span class="hljs-string">"__locale__"</span>, locale);<br>        }<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>不要忘了在<code>WebMvcConfigurer</code>中注册<code>MvcInterceptor</code>。现在，就可以在View中调用<code>MessageSource.getMessage()</code>方法来实现多语言：</p>
<figure class="highlight html hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/signin"</span>&gt;</span>{{ __messageSource__.getMessage('signin', null, __locale__) }}<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>上述这种写法虽然可行，但格式太复杂了。使用View时，要根据每个特定的View引擎定制国际化函数。在Pebble中，我们可以封装一个国际化函数，名称就是下划线<code>_</code>，改造一下创建<code>ViewResolver</code>的代码：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">ViewResolver <span class="hljs-title">createViewResolver</span><span class="hljs-params">(<span class="hljs-meta">@Autowired</span> ServletContext servletContext, <span class="hljs-meta">@Autowired</span> <span class="hljs-meta">@Qualifier("i18n")</span> MessageSource messageSource)</span> </span>{<br>    PebbleEngine engine = <span class="hljs-keyword">new</span> PebbleEngine.Builder()<br>            .autoEscaping(<span class="hljs-keyword">true</span>)<br>            .cacheActive(<span class="hljs-keyword">false</span>)<br>            .loader(<span class="hljs-keyword">new</span> ServletLoader(servletContext))<br>            <span class="hljs-comment">// 添加扩展:</span><br>            .extension(createExtension(messageSource))<br>            .build();<br>    PebbleViewResolver viewResolver = <span class="hljs-keyword">new</span> PebbleViewResolver();<br>    viewResolver.setPrefix(<span class="hljs-string">"/WEB-INF/templates/"</span>);<br>    viewResolver.setSuffix(<span class="hljs-string">""</span>);<br>    viewResolver.setPebbleEngine(engine);<br>    <span class="hljs-keyword">return</span> viewResolver;<br>}<br><br><span class="hljs-function"><span class="hljs-keyword">private</span> Extension <span class="hljs-title">createExtension</span><span class="hljs-params">(MessageSource messageSource)</span> </span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> AbstractExtension() {<br>        <span class="hljs-meta">@Override</span><br>        <span class="hljs-function"><span class="hljs-keyword">public</span> Map&lt;String, Function&gt; <span class="hljs-title">getFunctions</span><span class="hljs-params">()</span> </span>{<br>            <span class="hljs-keyword">return</span> Map.of(<span class="hljs-string">"_"</span>, <span class="hljs-keyword">new</span> Function() {<br>                <span class="hljs-function"><span class="hljs-keyword">public</span> Object <span class="hljs-title">execute</span><span class="hljs-params">(Map&lt;String, Object&gt; args, PebbleTemplate self, EvaluationContext context, <span class="hljs-keyword">int</span> lineNumber)</span> </span>{<br>                    String key = (String) args.get(<span class="hljs-string">"0"</span>);<br>                    List&lt;Object&gt; arguments = <span class="hljs-keyword">this</span>.extractArguments(args);<br>                    Locale locale = (Locale) context.getVariable(<span class="hljs-string">"__locale__"</span>);<br>                    <span class="hljs-keyword">return</span> messageSource.getMessage(key, arguments.toArray(), <span class="hljs-string">"???"</span> + key + <span class="hljs-string">"???"</span>, locale);<br>                }<br>                <span class="hljs-function"><span class="hljs-keyword">private</span> List&lt;Object&gt; <span class="hljs-title">extractArguments</span><span class="hljs-params">(Map&lt;String, Object&gt; args)</span> </span>{<br>                    <span class="hljs-keyword">int</span> i = <span class="hljs-number">1</span>;<br>                    List&lt;Object&gt; arguments = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();<br>                    <span class="hljs-keyword">while</span> (args.containsKey(String.valueOf(i))) {<br>                        Object param = args.get(String.valueOf(i));<br>                        arguments.add(param);<br>                        i++;<br>                    }<br>                    <span class="hljs-keyword">return</span> arguments;<br>                }<br>                <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;String&gt; <span class="hljs-title">getArgumentNames</span><span class="hljs-params">()</span> </span>{<br>                    <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;<br>                }<br>            });<br>        }<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这样，我们可以把多语言页面改写为：</p>
<figure class="highlight html hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/signin"</span>&gt;</span>{{ _('signin') }}<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>如果是带参数的多语言，需要把参数传进去：</p>
<figure class="highlight html hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">h5</span>&gt;</span>{{ _('copyright', 2020) }}<span class="hljs-tag">&lt;/<span class="hljs-name">h5</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>使用其它View引擎时，也应当根据引擎接口实现更方便的语法。</p>
<h3 id="切换Locale"><a href="#切换Locale" class="headerlink" title="切换Locale"></a>切换Locale</h3><p>最后，我们需要允许用户手动切换<code>Locale</code>，编写一个<code>LocaleController</code>来实现该功能：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Controller</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LocaleController</span> </span>{<br>    <span class="hljs-keyword">final</span> Logger logger = LoggerFactory.getLogger(getClass());<br><br>    <span class="hljs-meta">@Autowired</span><br>    LocaleResolver localeResolver;<br><br>    <span class="hljs-meta">@GetMapping("/locale/{lo}")</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">setLocale</span><span class="hljs-params">(<span class="hljs-meta">@PathVariable("lo")</span> String lo, HttpServletRequest request, HttpServletResponse response)</span> </span>{<br>        <span class="hljs-comment">// 根据传入的lo创建Locale实例:</span><br>        Locale locale = <span class="hljs-keyword">null</span>;<br>        <span class="hljs-keyword">int</span> pos = lo.indexOf(<span class="hljs-string">'_'</span>);<br>        <span class="hljs-keyword">if</span> (pos &gt; <span class="hljs-number">0</span>) {<br>            String lang = lo.substring(<span class="hljs-number">0</span>, pos);<br>            String country = lo.substring(pos + <span class="hljs-number">1</span>);<br>            locale = <span class="hljs-keyword">new</span> Locale(lang, country);<br>        } <span class="hljs-keyword">else</span> {<br>            locale = <span class="hljs-keyword">new</span> Locale(lo);<br>        }<br>        <span class="hljs-comment">// 设定此Locale:</span><br>        localeResolver.setLocale(request, response, locale);<br>        logger.info(<span class="hljs-string">"locale is set to {}."</span>, locale);<br>        <span class="hljs-comment">// 刷新页面:</span><br>        String referer = request.getHeader(<span class="hljs-string">"Referer"</span>);<br>        <span class="hljs-keyword">return</span> <span class="hljs-string">"redirect:"</span> + (referer == <span class="hljs-keyword">null</span> ? <span class="hljs-string">"/"</span> : referer);<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>在页面设计中，通常在右上角给用户提供一个语言选择列表。</p>
<h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h4><p>多语言支持需要从HTTP请求中解析用户的Locale，然后针对不同Locale显示不同的语言。Spring MVC应用程序通过<code>MessageSource</code>和<code>LocaleResolver</code>，配合View实现国际化。</p>
<h2 id="异步处理"><a href="#异步处理" class="headerlink" title="异步处理"></a>异步处理</h2><p>在Servlet模型中，每个请求都是由某个线程处理，然后，将响应写入IO流，发送给客户端。从开始处理请求，到写入响应完成，都是在同一个线程中处理的。</p>
<p>实现Servlet容器时，只要每处理一个请求，就创建一个新线程处理它，就能保证正确实现了Servlet线程模型。在实际产品中，例如Tomcat，总是通过线程池来处理请求，仍然符合一个请求从头到尾都由某一个线程处理。</p>
<p>这种线程模型非常重要，因为Spring的JDBC事务是基于<code>ThreadLocal</code>实现的，如果在处理过程中，一会由线程A处理，一会由线程B处理，那事务就乱套了。此外，很多安全认证也是基于<code>ThreadLocal</code>实现的，可以保证在处理请求的过程中，各个线程互不影响。</p>
<p>但是，如果一个请求处理的时间比较长，例如有几秒钟甚至更长，这种基于线程池的同步模型很快就把所有的线程耗尽，导致服务器无法响应新的请求。如果把长时间处理的请求改为异步处理，那么线程池的利用率会大大提高。Servlet从3.0规范开始添加了异步支持，允许对一个请求进行异步处理。</p>
<p>我们先来看看在Spring MVC中实现对请求进行异步处理的逻辑。首先建立一个Web工程，然后编辑<code>web.xml</code>文件如下：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">web-app</span> <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://xmlns.jcp.org/xml/ns/javaee"</span></span><br><span class="hljs-tag">    <span class="hljs-attr">xmlns:xsi</span>=<span class="hljs-string">"http://www.w3.org/2001/XMLSchema-instance"</span></span><br><span class="hljs-tag">    <span class="hljs-attr">xsi:schemaLocation</span>=<span class="hljs-string">"http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"</span></span><br><span class="hljs-tag">    <span class="hljs-attr">version</span>=<span class="hljs-string">"3.1"</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">display-name</span>&gt;</span>Archetype Created Web Application<span class="hljs-tag">&lt;/<span class="hljs-name">display-name</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">servlet</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-name</span>&gt;</span>dispatcher<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-class</span>&gt;</span>org.springframework.web.servlet.DispatcherServlet<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-class</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>contextClass<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>org.springframework.web.context.support.AnnotationConfigWebApplicationContext<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">init-param</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-name</span>&gt;</span>contextConfigLocation<span class="hljs-tag">&lt;/<span class="hljs-name">param-name</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">param-value</span>&gt;</span>com.itranswarp.learnjava.AppConfig<span class="hljs-tag">&lt;/<span class="hljs-name">param-value</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">init-param</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">load-on-startup</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">load-on-startup</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">async-supported</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">async-supported</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">servlet</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">servlet-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">servlet-name</span>&gt;</span>dispatcher<span class="hljs-tag">&lt;/<span class="hljs-name">servlet-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/*<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">servlet-mapping</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">web-app</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>和前面普通的MVC程序相比，这个<code>web.xml</code>主要有几点不同：</p>
<ul>
<li>不能再使用<code>&lt;!DOCTYPE ...web-app_2_3.dtd"&gt;</code>的DTD声明，必须用新的支持Servlet 3.1规范的XSD声明，照抄即可</li>
<li>对<code>DispatcherServlet</code>的配置多了一个<code>&lt;async-supported&gt;</code>，默认值是<code>false</code>，必须明确写成<code>true</code>，这样Servlet容器才会支持async处理</li>
</ul>
<p>然后在Controller中编写async处理逻辑。我们以ApiController为例，演示如何异步处理请求。</p>
<p>第一种async处理方式是返回一个Callable，Spring MVC自动把返回的Callable放入线程池执行，等待结果返回后再写入响应：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@GetMapping("/users")</span><br><span class="hljs-keyword">public</span> Callable&lt;List&lt;User&gt;&gt; users() {<br>    <span class="hljs-keyword">return</span> () -&gt; {<br>        <span class="hljs-comment">// 模拟3秒耗时:</span><br>        <span class="hljs-keyword">try</span> {<br>            Thread.sleep(<span class="hljs-number">3000</span>);<br>        } <span class="hljs-keyword">catch</span> (InterruptedException e) {<br>        }<br>        <span class="hljs-keyword">return</span> userService.getUsers();<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>第二种async处理方式是返回一个DeferredResult对象，然后在另一个线程中设置此对象的值并写入响应：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@GetMapping("/users/{id}")</span><br><span class="hljs-function"><span class="hljs-keyword">public</span> DeferredResult&lt;User&gt; <span class="hljs-title">user</span><span class="hljs-params">(<span class="hljs-meta">@PathVariable("id")</span> <span class="hljs-keyword">long</span> id)</span> </span>{<br>    DeferredResult&lt;User&gt; result = <span class="hljs-keyword">new</span> DeferredResult&lt;&gt;(<span class="hljs-number">3000L</span>); <span class="hljs-comment">// 3秒超时</span><br>    <span class="hljs-keyword">new</span> Thread(() -&gt; {<br>        <span class="hljs-comment">// 等待1秒:</span><br>        <span class="hljs-keyword">try</span> {<br>            Thread.sleep(<span class="hljs-number">1000</span>);<br>        } <span class="hljs-keyword">catch</span> (InterruptedException e) {<br>        }<br>        <span class="hljs-keyword">try</span> {<br>            User user = userService.getUserById(id);<br>            <span class="hljs-comment">// 设置正常结果并由Spring MVC写入Response:</span><br>            result.setResult(user);<br>        } <span class="hljs-keyword">catch</span> (Exception e) {<br>            <span class="hljs-comment">// 设置错误结果并由Spring MVC写入Response:</span><br>            result.setErrorResult(Map.of(<span class="hljs-string">"error"</span>, e.getClass().getSimpleName(), <span class="hljs-string">"message"</span>, e.getMessage()));<br>        }<br>    }).start();<br>    <span class="hljs-keyword">return</span> result;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>使用<code>DeferredResult</code>时，可以设置超时，超时会自动返回超时错误响应。在另一个线程中，可以调用<code>setResult()</code>写入结果，也可以调用<code>setErrorResult()</code>写入一个错误结果。</p>
<h3 id="使用Filter"><a href="#使用Filter" class="headerlink" title="使用Filter"></a>使用Filter</h3><p>当我们使用async模式处理请求时，原有的Filter也可以工作，但必须在<code>web.xml</code>中添加<code>&lt;async-supported&gt;</code>并设置为<code>true</code>。我们用两个Filter：SyncFilter和AsyncFilter分别测试：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">web-app</span> <span class="hljs-attr">...</span>&gt;</span><br>    ...<br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>sync-filter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-class</span>&gt;</span>com.itranswarp.learnjava.web.SyncFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-class</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>async-filter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-class</span>&gt;</span>com.itranswarp.learnjava.web.AsyncFilter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-class</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">async-supported</span>&gt;</span>true<span class="hljs-tag">&lt;/<span class="hljs-name">async-supported</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>sync-filter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/api/version<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter-mapping</span>&gt;</span><br><br>    <span class="hljs-tag">&lt;<span class="hljs-name">filter-mapping</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">filter-name</span>&gt;</span>async-filter<span class="hljs-tag">&lt;/<span class="hljs-name">filter-name</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">url-pattern</span>&gt;</span>/api/*<span class="hljs-tag">&lt;/<span class="hljs-name">url-pattern</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">filter-mapping</span>&gt;</span><br>    ...<br><span class="hljs-tag">&lt;/<span class="hljs-name">web-app</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>一个声明为支持<code>&lt;async-supported&gt;</code>的Filter既可以过滤async请求，也可以过滤正常的同步请求，而未声明<code>&lt;async-supported&gt;</code>的Filter无法支持async请求。如果一个普通的Filter遇到async请求时，会直接报错，因此，务必注意普通Filter的<code>&lt;url-pattern&gt;</code>不要匹配async请求路径。</p>
<p>在<code>logback.xml</code>配置文件中，我们输出格式加上<code>[%thread]</code>，可以输出当前线程的名称：</p>
<figure class="highlight xml hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span><br>    <span class="hljs-tag">&lt;<span class="hljs-name">appender</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"STDOUT"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ch.qos.logback.core.ConsoleAppender"</span>&gt;</span><br>        <span class="hljs-tag">&lt;<span class="hljs-name">layout</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"ch.qos.logback.classic.PatternLayout"</span>&gt;</span><br>            <span class="hljs-tag">&lt;<span class="hljs-name">Pattern</span>&gt;</span>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n<span class="hljs-tag">&lt;/<span class="hljs-name">Pattern</span>&gt;</span><br>        <span class="hljs-tag">&lt;/<span class="hljs-name">layout</span>&gt;</span><br>    <span class="hljs-tag">&lt;/<span class="hljs-name">appender</span>&gt;</span><br>    ...<br><span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span><br></code></pre></td></tr></tbody></table></figure>

<p>对于同步请求，每个Filter和ApiController都是由同一个线程执行。对于异步请求，AsyncFilter和ApiController是由同一个线程执行的，但是，返回响应的是另一个线程。</p>
<p>实际中，经常使用到的就是<code>DeferredResult</code>，因为返回<code>DeferredResult</code>时，可以设置超时、正常结果和错误结果，易于编写比较灵活的逻辑。使用async异步处理响应时，要时刻牢记，在另一个异步线程中的事务和Controller方法中执行的事务不是同一个事务，在Controller中绑定的<code>ThreadLocal</code>信息也无法在异步线程中获取。</p>
<p>此外，Servlet 3.0规范添加的异步支持是针对同步模型打了一个“补丁”，虽然可以异步处理请求，但高并发异步请求时，它的处理效率并不高，因为这种异步模型并没有用到真正的“原生”异步。Java标准库提供了封装操作系统的异步IO包<code>java.nio</code>，是真正的多路复用IO模型，可以用少量线程支持大量并发。使用NIO编程复杂度比同步IO高很多，因此我们很少直接使用NIO。相反，大部分需要高性能异步IO的应用程序会选择<a target="_blank" rel="noopener" href="https://netty.io/"><strong>Netty</strong></a>这样的框架，它基于NIO提供了更易于使用的API，方便开发异步应用程序。</p>
<h2 id="使用WebSocket"><a href="#使用WebSocket" class="headerlink" title="使用WebSocket"></a>使用WebSocket</h2><p>WebSocket是一种基于HTTP的长链接技术。传统的HTTP协议是一种请求-响应模型，如果浏览器不发送请求，那么服务器无法主动给浏览器推送数据。如果要定期给浏览器推送数据，例如股票行情，或者不定期给浏览器推送数据，例如在线聊天，基于HTTP协议实现这类需求，只能依靠浏览器的定时轮询，效率低且实时性不高。</p>
<p>因为HTTP本身是基于TCP连接的，所以，WebSocket在HTTP协议的基础上做了一个简单的升级，即建立TCP连接后，浏览器发送请求时，附带几个头：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">GET /chat HTTP/1.1<br>Host: www.example.com<br>Upgrade: websocket<br>Connection: Upgrade<br></code></pre></td></tr></tbody></table></figure>

<p>就表示客户端希望升级连接，变成长连接的WebSocket，服务器返回升级成功的响应：</p>
<figure class="highlight plaintext hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs plaintext">HTTP/1.1 101 Switching Protocols<br>Upgrade: websocket<br>Connection: Upgrade<br></code></pre></td></tr></tbody></table></figure>

<p>收到成功响应时表示WebSocket“握手”成功，这样，代表WebSocket的这个TCP连接将不会被服务器关闭，而是一直保持，服务器可随时向浏览器推送消息，浏览器也可以随时向服务器推送消息。双方推送消息既可以是文本消息，也可以是二进制消息，一般来说，绝大部分应用程序都会推送基于JSON的文本消息。</p>
<p>现代浏览器都已经支持WebSocket协议，服务器则需要底层框架支持。Java的Servelt规范从3.1开始支持WebSocket，所以，必须选择支持Servlet3.1或更高规范的Servlet容器，才能支持WebSocket。最新版本的Tomcat、Jetty等开源服务器均支持WebSocket。</p>
<p>我们以实际代码演示如何在Spring MVC中实现对WebSocket的支持。首先，我们需要在<code>pom.xml</code>中加入以下依赖：</p>
<ul>
<li>org.apache.tomcat.embed:tomcat-embed-websocket:9.0.26</li>
<li>org.springframework:spring-websocket:5.2.0.RELEASE</li>
</ul>
<p>第一项是嵌入式Tomcat支持WebSocket的组件，第二项是Spring封装的支持WebSocket的接口。</p>
<p>接下来，我们要在AppConfig中加入Spring Web对WebSocket的配置，此处我们要创建一个<code>WebSocketConfigurer</code>实例。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Bean</span><br><span class="hljs-function">WebSocketConfigurer <span class="hljs-title">createWebSocketConfigurer</span><span class="hljs-params">(</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Autowired</span> ChatHandler chatHandler,</span></span><br><span class="hljs-params"><span class="hljs-function">        <span class="hljs-meta">@Autowired</span> ChatHandshakeInterceptor chatInterceptor)</span></span><br><span class="hljs-function"></span>{<br>    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> WebSocketConfigurer() {<br>        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">registerWebSocketHandlers</span><span class="hljs-params">(WebSocketHandlerRegistry registry)</span> </span>{<br>            <span class="hljs-comment">// 把URL与指定的WebSocketHandler关联，可关联多个:</span><br>            registry.addHandler(chatHandler, <span class="hljs-string">"/chat"</span>).addInterceptors(chatInterceptor);<br>        }<br>    };<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>此实例在内部通过<code>WebSocketHandlerRegistry</code>注册能处理WebSocket的<code>WebSocketHandler</code>，以及可选的WebSocket拦截器<code>HandshakeInterceptor</code>。我们注入的这两个类都是自己编写的业务逻辑，后面我们详细讨论如何编写它们，这里只需关注浏览器连接到WebSocket的URL是<code>/chat</code>。</p>
<h3 id="处理WebSocket连接"><a href="#处理WebSocket连接" class="headerlink" title="处理WebSocket连接"></a>处理WebSocket连接</h3><p>和处理普通HTTP请求不同，没法用一个方法处理一个URL。Spring提供了<code>TextWebSocketHandler</code>和<code>BinaryWebSocketHandler</code>分别处理文本消息和二进制消息，这里我们选择文本消息作为聊天室的协议，因此，<code>ChatHandler</code>需要继承自<code>TextWebSocketHandler</code>：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatHandler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TextWebSocketHandler</span> </span>{<br>    ...<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>当浏览器请求一个WebSocket连接后，如果成功建立连接，Spring会自动调用<code>afterConnectionEstablished()</code>方法，任何原因导致WebSocket中断，Spring会自动调用<code>afterConnectionClosed()</code>方法，因此，覆写这两个方法即可处理连接成功和结束后的业务逻辑。</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatHandler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TextWebSocketHandler</span> </span>{<br>    <span class="hljs-comment">// 保存所有Client的WebSocket会话实例:</span><br>    <span class="hljs-keyword">private</span> Map&lt;String, WebSocketSession&gt; clients = <span class="hljs-keyword">new</span> ConcurrentHashMap&lt;&gt;();<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterConnectionEstablished</span><span class="hljs-params">(WebSocketSession session)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        <span class="hljs-comment">// 新会话根据ID放入Map:</span><br>        clients.put(session.getId(), session);<br>        session.getAttributes().put(<span class="hljs-string">"name"</span>, <span class="hljs-string">"Guest1"</span>);<br>    }<br><br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">afterConnectionClosed</span><span class="hljs-params">(WebSocketSession session, CloseStatus status)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        clients.remove(session.getId());<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>每个WebSocket会话以<code>WebSocketSession</code>表示，且已分配唯一ID。和WebSocket相关的数据，例如用户名称等，均可放入关联的<code>getAttributes()</code>中。</p>
<p>用实例变量<code>clients</code>持有当前所有的<code>WebSocketSession</code>是为了广播，即向所有用户推送同一消息时，可以这么写：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java">String json = ...<br>TextMessage message = <span class="hljs-keyword">new</span> TextMessage(json);<br><span class="hljs-keyword">for</span> (String id : clients.keySet()) {<br>    WebSocketSession session = clients.get(id);<br>    session.sendMessage(message);<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>我们发送的消息是序列化后的JSON，可以用ChatMessage表示：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatMessage</span> </span>{<br>	<span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> timestamp;<br>	<span class="hljs-keyword">public</span> String name;<br>  <span class="hljs-keyword">public</span> String text;<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>每收到一个用户的消息后，我们就需要广播给所有用户：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatHandler</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">TextWebSocketHandler</span> </span>{<br>    ...<br>    <span class="hljs-meta">@Override</span><br>    <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleTextMessage</span><span class="hljs-params">(WebSocketSession session, TextMessage message)</span> <span class="hljs-keyword">throws</span> Exception </span>{<br>        String s = message.getPayload();<br>        String r = ... <span class="hljs-comment">// 根据输入消息构造待发送消息</span><br>        broadcastMessage(r); <span class="hljs-comment">// 推送给所有用户</span><br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>如果要推送给指定的几个用户，那就需要在<code>clients</code>中根据条件查找出某些<code>WebSocketSession</code>，然后发送消息。</p>
<p>注意到我们在注册WebSocket时还传入了一个<code>ChatHandshakeInterceptor</code>，这个类实际上可以从<code>HttpSessionHandshakeInterceptor</code>继承，它的主要作用是在WebSocket建立连接后，把HttpSession的一些属性复制到WebSocketSession，例如，用户的登录信息等：</p>
<figure class="highlight java hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Component</span><br><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ChatHandshakeInterceptor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HttpSessionHandshakeInterceptor</span> </span>{<br>    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ChatHandshakeInterceptor</span><span class="hljs-params">()</span> </span>{<br>        <span class="hljs-comment">// 指定从HttpSession复制属性到WebSocketSession:</span><br>        <span class="hljs-keyword">super</span>(List.of(UserController.KEY_USER));<br>    }<br>}<br></code></pre></td></tr></tbody></table></figure>

<p>这样，在<code>ChatHandler</code>中，可以从<code>WebSocketSession.getAttributes()</code>中获取到复制过来的属性。</p>
<h3 id="客户端开发"><a href="#客户端开发" class="headerlink" title="客户端开发"></a>客户端开发</h3><p>在完成了服务器端的开发后，我们还需要在页面编写一点JavaScript逻辑：</p>
<figure class="highlight js hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 创建WebSocket连接:</span><br><span class="hljs-keyword">var</span> ws = <span class="hljs-keyword">new</span> WebSocket(<span class="hljs-string">'ws://'</span> + location.host + <span class="hljs-string">'/chat'</span>);<br><span class="hljs-comment">// 连接成功时:</span><br>ws.addEventListener(<span class="hljs-string">'open'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{<br>    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'websocket connected.'</span>);<br>});<br><span class="hljs-comment">// 收到消息时:</span><br>ws.addEventListener(<span class="hljs-string">'message'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) </span>{<br>    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'message: '</span> + event.data);<br>    <span class="hljs-keyword">var</span> msgs = <span class="hljs-built_in">JSON</span>.parse(event.data);<br>    <span class="hljs-comment">// <span class="hljs-doctag">TODO:</span></span><br>});<br><span class="hljs-comment">// 连接关闭时:</span><br>ws.addEventListener(<span class="hljs-string">'close'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{<br>    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'websocket closed.'</span>);<br>});<br><span class="hljs-comment">// 绑定到全局变量:</span><br><span class="hljs-built_in">window</span>.chatWs = ws;<br></code></pre></td></tr></tbody></table></figure>

<p>用户可以在连接成功后任何时候给服务器发送消息：</p>
<figure class="highlight js hljs"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> inputText = <span class="hljs-string">'Hello, WebSocket.'</span>;<br><span class="hljs-built_in">window</span>.chatWs.send(<span class="hljs-built_in">JSON</span>.stringify({<span class="hljs-attr">text</span>: inputText}));<br></code></pre></td></tr></tbody></table></figure>

<p>最后，连调浏览器和服务器端，如果一切无误，可以开多个不同的浏览器测试WebSocket的推送和广播。</p>
<blockquote>
<p>和上一节我们介绍的异步处理类似，Servlet的线程模型并不适合大规模的长链接。基于NIO的Netty等框架更适合处理WebSocket长链接，我们将在后面介绍。</p>
<p>通过<code>spring-websocket</code>可以简化WebSocket的开发。</p>
</blockquote>
</body></html>
    
    </div>
    
    
    <div class="columns is-mobile is-multiline article-nav">
        <span class="column is-12-mobile is-half-desktop  article-nav-prev">
            
            <a href="/Study/Java/Spring/zh-cn/%E9%9B%86%E6%88%90%E7%AC%AC%E4%B8%89%E6%96%B9%E7%BB%84%E4%BB%B6/">集成第三方组件</a>
            
        </span>
        <span class="column is-12-mobile is-half-desktop  article-nav-next">
            
            <a href="/Study/Java/Spring/zh-cn/%E8%AE%BF%E9%97%AE%E6%95%B0%E6%8D%AE%E5%BA%93/">访问数据库</a>
            
        </span>
    </div>
    
</article>


<div class="sharebox">
    
<div class="sharethis-inline-share-buttons"></div>
<script type='text/javascript' src='//platform-api.sharethis.com/js/sharethis.js#property=608c1408daac690012507aa2&amp;product=sop' async='async'></script>

</div>



    </div>
</section>
    <footer class="footer">
    <div class="container">
        <div class="columns content">
            <div class="column is-narrow has-text-centered">
                &copy; 2021 wanzixin&nbsp;
                Powered by <a href="http://hexo.io/" target="_blank">Hexo</a> & <a
                        target="_blank" rel="noopener" href="http://github.com/ppoffice/hexo-theme-minos">Minos</a>
            </div>
            <div class="column is-hidden-mobile"></div>

            
            <div class="column is-narrow">
                <div class="columns is-mobile is-multiline is-centered">
                
                    
                <a class="column is-narrow has-text-black" title="GitHub" target="_blank" rel="noopener" href="https://github.com/ppoffice/hexo-theme-minos">
                    
                    GitHub
                    
                </a>
                
                </div>
            </div>
            
            
<div class="column is-narrow has-text-centered">
    <div class="dropdown is-up is-right is-hoverable" style="margin-top: -0.2em;">
        <div class="dropdown-trigger">
            <button class="button is-small" aria-haspopup="true" aria-controls="dropdown-menu7">
                <span class="icon">
                    <i class="fas fa-globe"></i>
                </span>
                <span>简体中文</span>
                <span class="icon is-small">
            <i class="fas fa-angle-down" aria-hidden="true"></i>
          </span>
            </button>
        </div>
        <div class="dropdown-menu has-text-left" role="menu">
            <div class="dropdown-content">
            
                <a href="/Study/Java/Spring/zh-cn/%E5%BC%80%E5%8F%91Web%E5%BA%94%E7%94%A8/" class="dropdown-item">
                    English
                </a>
            
                <a href="/zh-cn/Study/Java/Spring/zh-cn/%E5%BC%80%E5%8F%91Web%E5%BA%94%E7%94%A8/" class="dropdown-item">
                    简体中文
                </a>
            
            </div>
        </div>
    </div>
</div>

        </div>
    </div>
</footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment-with-locales.min.js"></script>

<!-- test if the browser is outdated -->
<div id="outdated">
    <h6>Your browser is out-of-date!</h6>
    <p>Update your browser to view this website correctly. <a id="btnUpdateBrowser" target="_blank" rel="noopener" href="http://outdatedbrowser.com/">Update my browser now </a></p>
    <p class="last"><a href="#" id="btnCloseUpdateBrowser" title="Close">&times;</a></p>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/outdated-browser/1.1.5/outdatedbrowser.min.js"></script>
<script>
    $(document).ready(function () {
        // plugin function, place inside DOM ready function
        outdatedBrowser({
            bgColor: '#f25648',
            color: '#ffffff',
            lowerThan: 'flex'
        })
    });
</script>

<script>
    window.FontAwesomeConfig = {
        searchPseudoElements: true
    }
    moment.locale("zh-CN");
</script>


    
    
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<script>
    MathJax.Hub.Config({
        "HTML-CSS": {
            matchFontHeight: false
        },
        SVG: {
            matchFontHeight: false
        },
        CommonHTML: {
            matchFontHeight: false
        },
        tex2jax: {
            inlineMath: [
                ['$','$'],
                ['\\(','\\)']
            ]
        }
    });
</script>

    
    
    
    
<script src="//cdnjs.cloudflare.com/ajax/libs/lightgallery/1.6.8/js/lightgallery-all.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/justifiedGallery/3.6.5/js/jquery.justifiedGallery.min.js"></script>
<script>
    (function ($) {
        $(document).ready(function () {
            if (typeof($.fn.lightGallery) === 'function') {
                $('.article.gallery').lightGallery({ selector: '.gallery-item' });
            }
            if (typeof($.fn.justifiedGallery) === 'function') {
                $('.justified-gallery').justifiedGallery();
            }
        });
    })(jQuery);
</script>

    
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.0/clipboard.min.js"></script>
    <style>
        .hljs {
            position: relative;
        }

        .hljs .clipboard-btn {
            float: right;
            color: #9a9a9a;
            background: none;
            border: none;
            cursor: pointer;
        }

        .hljs .clipboard-btn:hover {
          color: #8a8a8a;
        }

        .hljs > .clipboard-btn {
            display: none;
            position: absolute;
            right: 4px;
            top: 4px;
        }

        .hljs:hover > .clipboard-btn {
            display: inline;
        }

        .hljs > figcaption > .clipboard-btn {
            margin-right: 4px;
        }
    </style>
    <script>
      $(document).ready(function () {
        $('figure.hljs').each(function(i, figure) {
          var codeId = 'code-' + i;
          var code = figure.querySelector('.code');
          var copyButton = $('<button>Copy <i class="far fa-clipboard"></i></button>');
          code.id = codeId;
          copyButton.addClass('clipboard-btn');
          copyButton.attr('data-clipboard-target-id', codeId);

          var figcaption = figure.querySelector('figcaption');

          if (figcaption) {
            figcaption.append(copyButton[0]);
          } else {
            figure.prepend(copyButton[0]);
          }
        })

        var clipboard = new ClipboardJS('.clipboard-btn', {
          target: function(trigger) {
            return document.getElementById(trigger.getAttribute('data-clipboard-target-id'));
          }
        });
        clipboard.on('success', function(e) {
          e.clearSelection();
        })
      })
    </script>

    
    

    



<script src="/js/script.js"></script>


    
    <div class="searchbox ins-search">
    <div class="searchbox-mask"></div>
    <div class="searchbox-container ins-search-container">
        <div class="searchbox-input-wrapper">
            <input type="text" class="searchbox-input ins-search-input" placeholder="站内搜索" />
            <span class="searchbox-close ins-close ins-selectable"><i class="fa fa-times-circle"></i></span>
        </div>
        <div class="searchbox-result-wrapper ins-section-wrapper">
            <div class="ins-section-container"></div>
        </div>
    </div>
</div>
<script>
    (function (window) {
        var INSIGHT_CONFIG = {
            TRANSLATION: {
                POSTS: '文章',
                PAGES: '页面',
                CATEGORIES: '分类',
                TAGS: '标签',
                UNTITLED: '(无标题)',
            },
            CONTENT_URL: '/content.zh-cn.json',
        };
        window.INSIGHT_CONFIG = INSIGHT_CONFIG;
    })(window);
</script>

<script src="/js/insight.js"></script>

    
</body>
</html>